﻿using System;
using System.Collections.Generic;
using System.Reflection;

namespace TNCGameEngine.Scripting
{

    internal abstract class Cmd
    {
        public enum Level
        {
            Top,
            Args,
            Index
        }

        protected const string OperationError = "Operation '{0}' error between objects '{1}' and '{2}'.";
                
        //public Cmd Parent = null;
        public List<Cmd> Children = new List<Cmd>();

        public CmdIf If = null;
        public virtual bool Execute
        {
            get { return ((this.If == null) || this.If.Evaluate); }
        }

        public abstract int Eval(ScriptFunction sfunc);

        public void AddChild(Cmd c)
        {
            this.Children.Add(c);
            //c.Parent = this;
        }

        protected void RemoveFromTemp(ScriptFunction sfunc, int i)
        {
            for (int j = 0; j < i; j++) 
            {
                sfunc.Temps.Pop();
            }
        }

    }

    internal class CmdValue : Cmd
    {

        public Type Type = null;
        public object Value = null;
        public bool Var = false;
        public string Name = string.Empty;

        public CmdValue(Type type, object Value)
        {
            this.Type = type;
            this.Value = Value;
        }

        public CmdValue(bool var, string name)
        {
            this.Var = var;
            this.Name = name;
        }

        public override int Eval(ScriptFunction sfunc)
        {
            if (this.Var)
            {
                object o;
                if (sfunc.Objects.TryGetValue(this.Name, out o))
                {
                    sfunc.Temps.Push(o);
                    return 1;
                }
                throw new Exception(string.Format("Variable object '{0}' cannote be found on the stack.", this.Name));
            }
            else
            {
                sfunc.Temps.Push(this.Value);
                return 1;
            }
        }

    }

    internal class CmdExpr : Cmd
    {

        public enum Operation
        {
            NoOp,
            Add,
            Subtract,
            Divide,
            Multiply,
            Mod
        }

        public Operation Op = Operation.Add;

        public CmdExpr(Operation op)
        {
            this.Op = op;
        }

        public override int Eval(ScriptFunction sfunc)
        {
            int i = 0;
            foreach (Cmd c in this.Children)
            {
                i += c.Eval(sfunc);
            }
            if (i == 2)
            {

                object tmp1 = sfunc.Temps.Pop();
                object tmp2 = sfunc.Temps.Pop();

                switch (this.Op)
                {

                    case Operation.Add:
                        sfunc.Temps.Push(this.OperationAdd(tmp1, tmp2));
                        return 1;

                    case Operation.Subtract:
                        sfunc.Temps.Push(this.OperationSubtract(tmp1, tmp2));
                        return 1;

                    case Operation.Divide:
                        sfunc.Temps.Push(this.OperationDivide(tmp1, tmp2));
                        return 1;

                    case Operation.Multiply:
                        sfunc.Temps.Push(this.OperationMultiply(tmp1, tmp2));
                        return 1;

                    case Operation.Mod:
                        sfunc.Temps.Push(this.OperationMod(tmp1, tmp2));
                        return 1;

                    default:
                        return 0;

                }

            }
            else
            {
                return i;
            }
            
        }

        private object OperationAdd(object t1, object t2)
        {
            try
            {
                //object o = Convert.ChangeType(t1.Value, t1.Type) + Convert.ChangeType(t2.Value, t2.Type);
                //return new object(t1.Value
                return null;
            }
            catch
            {
                throw new Exception(string.Format(Cmd.OperationError, "+", t1, t2));
            }
        }

        private object OperationSubtract(object t1, object t2)
        {
            try
            {
                return null;
            }
            catch
            {
                throw new Exception(string.Format(Cmd.OperationError, "-", t1, t2));
            }
        }

        private object OperationDivide(object t1, object t2)
        {
            try
            {
                return null;
            }
            catch
            {
                throw new Exception(string.Format(Cmd.OperationError, "/", t1, t2));
            }
        }

        private object OperationMultiply(object t1, object t2)
        {
            try
            {
                return null;
            }
            catch
            {
                throw new Exception(string.Format(Cmd.OperationError, "*", t1, t2));
            }
        }

        private object OperationMod(object t1, object t2)
        {
            try
            {
                return null;
            }
            catch
            {
                throw new Exception(string.Format(Cmd.OperationError, "%", t1, t2));
            }
        }

    }

    internal class CmdMethod : Cmd
    {

        public List<string> Names = null;

        public CmdMethod(List<string> names)
        {
            this.Names = names;
        }

        public override int Eval(ScriptFunction sfunc)
        {
            int i = 0;
            foreach (Cmd c in this.Children)
            {
                i += c.Eval(sfunc);
            }
            if (i == this.Children.Count)
            {
                object[] param = new object[i];
                for (int j = 0; j < i; j++)
                {
                    param[i - j - 1] = sfunc.Temps.Pop();
                }
                i = this.InvokeMethod(sfunc, param);
            }
            return i;
        }

        private int InvokeMethod(ScriptFunction sfunc, object[] param)
        {
            
            string curr = this.Names[0];
            object o;
            if (sfunc.Objects.TryGetValue(curr, out o))
            {

                if (this.Names.Count > 1)
                {

                    object obj = o;

                    int len = this.Names.Count - 1;
                    for (int i = 1; i < this.Names.Count; i++)
                    {
                        curr = this.Names[i];
                        if (i < len)
                        {
                            PropertyInfo pi = obj.GetType().GetProperty(curr);
                            if (pi != null)
                            {
                                obj = pi.GetValue(obj, null);
                                continue;
                            }
                            else
                            {
                                FieldInfo fi = o.GetType().GetField(curr);
                                if (fi != null)
                                {
                                    obj = fi.GetValue(obj);
                                }
                                else
                                {
                                    throw new Exception(string.Format("Property or Field '{0}' cannot be found.", curr));
                                }
                            }

                        }
                        else
                        {
                            
                            MethodInfo mi = obj.GetType().GetMethod(curr);
                            if (mi != null)
                            {
                                object result = mi.Invoke(obj, param);
                                if (mi.ReturnType != null)
                                {
                                    sfunc.Temps.Push(result);
                                    return 1;
                                }
                                else
                                {
                                    return 0;
                                }
                            }
                            else
                            {
                                throw new Exception(string.Format("Method '{0}' cannot be found.", curr));
                            }

                        }

                    }

                }

            }

            throw new NullReferenceException(string.Format("Reference object '{0}' cannot be null.", curr));

        }
        
    }

    internal class CmdAssign : Cmd
    {

        public List<string> Names = null;

        public CmdAssign(List<string> names)
        {
            this.Names = names;
        }

        public override int Eval(ScriptFunction sfunc)
        {
            int i = 0;
            foreach (Cmd c in this.Children)
            {
                i += c.Eval(sfunc);
            }
            if (i == 1)
            {
                this.SetProperty(sfunc, sfunc.Temps.Pop());
                this.RemoveFromTemp(sfunc, --i);
            }
            return i;
        }

        private void SetProperty(ScriptFunction sfunc, object val)
        {
            
            string curr = this.Names[0];
            object o;
            if (sfunc.Objects.TryGetValue(curr, out o))
            {

                if (this.Names.Count == 1)
                {
                    sfunc.Objects[curr] = val;
                    //sfunc.Objects.Remove(curr);
                    //sfunc.Objects.Add(curr, val);
                    return;
                }
                else
                {

                    object obj = o;

                    int len = this.Names.Count - 1;
                    for (int i = 1; i < this.Names.Count; i++)
                    {
                        curr = this.Names[i];
                        PropertyInfo pi = obj.GetType().GetProperty(curr);
                        if (pi != null)
                        {
                            if (i < len)
                            {
                                obj = pi.GetValue(obj, null);
                                continue;
                            }
                            else
                            {
                                try
                                {
                                    pi.SetValue(obj, val, null);
                                    return;
                                }
                                catch
                                {
                                    throw new Exception(string.Format("Cannot assign value to property '{0}'.", curr));
                                }                                
                            }
                        }
                        else
                        {
                            FieldInfo fi = o.GetType().GetField(curr);
                            if (fi != null)
                            {
                                if (i < len)
                                {
                                    obj = fi.GetValue(obj);
                                    continue;
                                }
                                else
                                {
                                    try
                                    {
                                        fi.SetValue(obj, val);
                                    }
                                    catch
                                    {
                                        throw new Exception(string.Format("Cannot assign value tp field '{0}'.", curr));
                                    }
                                    return;
                                }
                            }
                            else
                            {
                                throw new Exception(string.Format("Property or Field '{0}' cannot be found.", curr));
                            }
                        }

                    }

                }

            }
            else if (this.Names.Count == 1)
            {
                sfunc.Objects.Add(curr, val);
                return;
            }

            throw new NullReferenceException(string.Format("Reference object '{0}' cannot be null.", curr));
            
        }

    }

    internal class CmdReturn : Cmd
    {

        public bool Returning = false;

        public CmdReturn(bool returning)
        {
            this.Returning = returning;
        }

        public override int Eval(ScriptFunction sfunc)
        {
            sfunc.Returned = true;
            int i = 0;
            if (this.Returning)
            {
                foreach (Cmd c in this.Children)
                {
                    i += c.Eval(sfunc);
                }
                sfunc.ReturnValue = sfunc.Temps.Pop();
                this.RemoveFromTemp(sfunc, --i);
            }
            return 0;
        }

    }

    internal class CmdProp : Cmd
    {

        public bool Returning = false;

        public CmdProp(bool returning)
        {
            this.Returning = returning;
        }

        public override int Eval(ScriptFunction sfunc)
        {
            sfunc.Returned = true;
            int i = 0;
            if (this.Returning)
            {
                foreach (Cmd c in this.Children)
                {
                    i += c.Eval(sfunc);
                }
                sfunc.ReturnValue = sfunc.Temps.Pop();
                this.RemoveFromTemp(sfunc, --i);
            }
            return 0;
        }

    }

    internal class CmdBool : Cmd
    {

        public bool Value = false;
        private bool Evaluated = false;

        public CmdBool(CmdExpr child)
        {
            this.Children.Add(child);
        }

        public CmdBool(CmdValue child)
        {
            this.Children.Add(child);
        }

        public CmdBool(bool evaluated, bool val = false)
        {
            this.Evaluated = evaluated;
            this.Value = val;
        }

        public override int Eval(ScriptFunction sfunc)
        {
            if (this.Evaluated)
            {
                sfunc.Temps.Push(this.Value);
                return 1;
            }
            else
            {
                int i = 0;
                foreach (Cmd c in this.Children)
                {
                    i += c.Eval(sfunc);
                }
                return i;
            }
        }

    }

    internal class CmdIf : Cmd
    {

        private bool evaluate = false;
        public virtual bool Evaluate
        {
            get { return this.evaluate; }
        }

        public CmdBool Condition = null;
        public CmdElse Else = null;

        public CmdIf(CmdBool cond)
        {
            this.Condition = cond;
        }

        public override int Eval(ScriptFunction sfunc)
        {
            if (this.Condition.Eval(sfunc) == 1)
            {
                this.evaluate = (bool)sfunc.Temps.Pop();
            }
            return 0;
        }

        public CmdIf GetParentIf()
        {
            return (this.Else != null) ? this.Else : this;
        }

    }

    internal class CmdElse : CmdIf
    {

        private CmdIf IfCmd = null;

        public override bool Evaluate
        {
            get { return !this.IfCmd.Evaluate; }
        }

        public override bool Execute
        {
            get { return false; }
        }
                
        public CmdElse(CmdIf ifcmd)
            : base(null)
        {
            this.IfCmd = ifcmd;
        }

        public override int Eval(ScriptFunction sfunc)
        {
            return 0;
        }

    }

    internal class CmdEndIf : Cmd
    {

        public CmdIf IfCmd = null;

        public override bool Execute
        {
            get { return false; }
        }

        public CmdEndIf(CmdIf cif)
        {
            this.IfCmd = cif;
        }

        public override int Eval(ScriptFunction sfunc)
        {            
            return 0;
        }

    }

}
